package engine;

import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import ui.SimulationUI;
import utilities.MatLabCOM;
import utilities.RandomNumeric;

public class GeneticAlgorithm implements Runnable {

    private Problem _problem;
    private Population _population;
    private double _mutateRate;
    private double _mutateIntensity;
    private MutateType _mutateType;
    private double _selectionBestRatio;
    private MatLabCOM _matlab;
    private long _startTime;
    private long _timeOut;
    private Boolean _timeIsUp;
    private SimulationUI _gui;
    private RandomNumeric _randomizer;
    private Boolean _stopThead;

    public GeneticAlgorithm(RandomNumeric randomizer, SimulationUI gui, MutateType mutateType, 
            double mutateRate, double mutateIntensity,
            double selectionBestRatio, long timeOut, long startTime) {
        _gui = gui;
        _mutateType = mutateType;
        _mutateRate = mutateRate;
        _mutateIntensity = mutateIntensity;
        _selectionBestRatio = selectionBestRatio;
        _timeOut = timeOut;
        _startTime = startTime;
        _randomizer = randomizer;
        _stopThead = false;
    }

    class ElapsedTimeUpdater extends TimerTask {

        private long _startTime;
        private SimulationUI _gui;
        final int UPDATE_PERIOD_MS = 500;

        public ElapsedTimeUpdater(long startTime, SimulationUI gui) {
            _startTime = startTime;
            _gui = gui;
        }

        @Override
        public void run() {
            _gui.updateElapsedTime((int) (System.currentTimeMillis() - _startTime));
        }
    }

    public Population getPopulation() {
        return _population;
    }

    public void setPopulation(Population _population) {
        this._population = _population;
    }

    public void setMatlab(MatLabCOM matlab) {
        _matlab = matlab;
    }

    public void setProblem(Problem _problem) {
        this._problem = _problem;
    }

    public Problem getProblem() {
        return _problem;
    }

    public Population tournament() {
        Population bestCandidates = new Population();

        int finalSize = (int) (_population.getCandidates().size() * _selectionBestRatio);
        if (finalSize == 0) {
            finalSize = 1;
        }

        bubbleSort(finalSize);
        for (int i = 0; i < finalSize; i++) {
            bestCandidates.addCandidate(_population.getCandidates().get(i));
        }

        return bestCandidates;
    }

    /**
     * Cette méthode permet de mettre en fin de tableau les pires candidats
     * possibles via un tri a bulle afin qu'ils ne soient pas dans le %age de
     * selection désiré
     *
     * @param finalLength la taille de la population à selectionner
     */
    public void bubbleSort(int finalLength) {
        Population pop = _population;

        int a = 1;
        boolean sorted = false;
        if (finalLength >= pop.getCandidates().size() / 2) {
            while (!sorted && pop.getCandidates().size() - a >= finalLength) {
                sorted = true;
                for (int i = 0; i < pop.getCandidates().size() - a; i++) {
                    if (pop.getCandidates().get(i).getFitnessValue() > pop.getCandidates().get(i + 1).getFitnessValue()) {
                        Candidate tmp = pop.getCandidates().get(i);
                        pop.setCandidate(i, pop.getCandidates().get(i + 1));
                        pop.setCandidate(i + 1, tmp);
                        sorted = false;
                    }
                }
                a++;
            }
        } else {
            while (!sorted && a <= finalLength) {
                sorted = true;
                for (int i = pop.getCandidates().size() - 1; i > a; i--) {
                    if (pop.getCandidates().get(i).getFitnessValue() < pop.getCandidates().get(i - 1).getFitnessValue()) {
                        Candidate tmp = pop.getCandidates().get(i);
                        pop.setCandidate(i, pop.getCandidates().get(i - 1));
                        pop.setCandidate(i - 1, tmp);
                        sorted = false;
                    }
                }
                a++;
            }
        }
    }

    public Candidate best(Candidate one, Candidate two) {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public Candidate crossover(Candidate one, Candidate two) {
        ArrayList<Double> result = new ArrayList<>();
        int size = one.getValues().size();
        int range = size / 2;

        for (int i = 0; i < size; i++) {
            if (i < range) {
                result.add(one.getValues().get(i));
            } else {
                result.add(two.getValues().get(i));
            }
        }
        return new Candidate(result);
    }

    public Population crossover() {

        Population result = new Population();
        List<Candidate> candidates = _population.getCandidates();

        for (int i = 0; i < candidates.size() - 1; i++) {
            result.addCandidate(crossover(candidates.get(i), candidates.get(i + 1)));
        }

        return result;
    }

    public Candidate mutateSwap(Candidate candidate) {
        ArrayList<Double> result = candidate.getValues();
        int size = result.size();
        int intensity = (int) (size * _mutateIntensity);
        double tmp;
        int pickedFirst, pickedSecond;

        for (int i = 0; i < intensity; i++) {
            pickedFirst = _randomizer.IntegerInInterval(0, size);
            tmp = result.get(pickedFirst);
            pickedSecond = _randomizer.IntegerInInterval(0, size);
            result.set(pickedFirst, result.get(pickedSecond));
            result.set(pickedSecond, tmp);
        }

        return new Candidate(result);
    }

    public Candidate mutateSubsitute(Candidate candidate) {
        ArrayList<Double> result = candidate.getValues();
        int size = result.size();
        int intensity = (int) (size * _mutateIntensity);
        double tmp;
        int picked;
        Constraint constr;
        double borneSup;
        double borneInf;

        for (int i = 0; i < intensity; i++) {
            picked = _randomizer.IntegerInInterval(0, size);
            constr = _problem.getConstraint();
            borneSup = constr.getBornesSup().get(picked);
            borneInf = constr.getBornesInf().get(picked);
            tmp = _randomizer.doubleInInterval(borneInf, borneSup);
            result.set(picked, tmp);
        }

        return new Candidate(result);
    }

    public Population mutate() {
        ArrayList<Candidate> candidates = _population.getCandidates();

        int size = candidates.size();
        int mutationRate = (int) (_mutateRate * size);
        int mutantIndex;
        
        //System.out.println("mutationRate="+mutationRate);
        
        //Stop les mutation pour les derniers survivants pour garantir au moins un candidat respectant les contraintes
        if(_population.size() <= 10) mutationRate = 0;
        
        
        for (int i = 0; i < mutationRate; i++) {
            mutantIndex = _randomizer.IntegerInInterval(0, size);
            if (_mutateType == MutateType.MUTATE_SWAP) {
                candidates.set(mutantIndex, mutateSwap(candidates.get(mutantIndex)));
            } else if (_mutateType == MutateType.MUTATE_SUBSTITUTE) {
                candidates.set(mutantIndex, mutateSubsitute(candidates.get(mutantIndex)));
            }
        }

        return new Population(candidates);
    }

    /*
     * Calcule la fitness de la population et met à jour le meilleur candidat et valeur de F() par ce candidat
     */
    private void calculateFitness() {
        Double currentFitness;

        Candidate bestCandidate = _population.getCandidates().get(0);
        Double bestFitness = _matlab.evaluate(_problem.getF(), bestCandidate);

        boolean curCandMatchConstrains;
        
        for (Candidate c : _population.getCandidates()) {
            currentFitness = _matlab.evaluate(_problem.getF(), c);
            c.setFitnessValue(currentFitness);
            
            //Match contraintes ?
            curCandMatchConstrains = c.matchEquations(_problem.getConstraint().getEquations(), _matlab);
            
            //Meilleur candidat
            if (currentFitness < bestFitness && curCandMatchConstrains) {
                bestFitness = currentFitness;
                bestCandidate = c;
            }
        }
        
        _problem.setBestCandidate(bestCandidate);
        _problem.setBestFitness(bestFitness);
    }

    /**
     *
     * @return Vrai s'il reste moins de 2 individus
     */
    private Boolean notEnoughPeople() {
        return _population.size() < 2;
    }

    /**
     * @return Vrai si le temps est écoulé
     */
    private Boolean timeIsUp() {
        _timeIsUp = (System.currentTimeMillis() - _startTime) >= _timeOut;
        return _timeIsUp;
    }

    private void runAlgo() {
        //Mis en place du chronometre
        ElapsedTimeUpdater task = new ElapsedTimeUpdater(_startTime, _gui);
        Timer timer = new Timer();
        timer.schedule(task, 0, task.UPDATE_PERIOD_MS);

        double progress;
        int initialPopSize = _population.size();

        do {
            _population = crossover();
            _population = mutate();
            calculateFitness();
            _population = tournament();

            //Ralentir algo pour tests
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
            }

            progress = _population.size() <= 1 ? 100 : (1 - (double) _population.size() / initialPopSize) * 100;
            _gui.updateProgressAlgo((int) progress);

        } while (!notEnoughPeople() && !timeIsUp() && !_stopThead);

        //Fin de boucle
        if (_timeIsUp) {
            System.out.println("Timeout.");
        } else if (_stopThead) {
            System.out.println("Stopped.");
        } else {
            System.out.println("Finished.");
        }

        //Update UI
        _gui.updateSolution(_problem.getBestFitness(), _problem.getBestCandidate());

        //Arret du chronometre
        timer.cancel();
    }

    /*
     * L'algorithme est exécuté de manière asynchrone afin de ne pas gêler l'interface
     */
    @Override
    public void run() {
        runAlgo();
    }

    public void stopThread() {
        _stopThead = true;
    }
}
